﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-12-05 18:10
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using BassDrop.Core.Common.Data.IO.Data;
using BassDrop.Core.Common.Data.IO.Portable;
using BassDrop.Core.Common.Media;
using BassDrop.Core.Common.Media.Playlists;
using BassDrop.Core.Common.Services;
using BassDrop.Core.Common.Utilities;

namespace BassDrop.Core.Common.Data.IO
{
	/// <summary>
	/// Provides reading and writing functions for eXtensible PlayList Files
	/// </summary>
	public static class XplfIO // TODO: XplfIO pluginba
	{
		/// <summary>
		/// The extension of XPLF files
		/// </summary>
		public readonly static string FileExtension = ".xplf";

		/// <summary>
		/// Reads an eXtensible PlayList File on the passed Uri and creates a Playlist
		/// from it
		/// </summary>
		/// <param name="uri"></param>
		/// <returns></returns>
		public static Playlist Read( Uri uri )
		{
			using ( var stream = ServiceCenter.FileHandler.GetStream( uri.LocalPath, PortableFileMode.Open, PortableFileAccess.Read, PortableFileShare.Read ) )
			{
				return Read( stream, new Uri( uri, "." ), ServiceCenter.FileHandler.GetFileName( uri.LocalPath ).SubstringUntilLast( "." ) );
			}
		}

		/// <summary>
		/// Reads an eXtensible PlayList File on the passed Stream and creates a Playlist
		/// from it
		/// </summary>
		/// <param name="stream"></param>
		/// <param name="workingDir">the path for which the local paths will be considered relative to</param>
		/// <param name="fileName">the name of the file being read without extension. It is used as the name of the playlist if the attribute is missing</param>
		/// <returns></returns>
		public static Playlist Read( Stream stream, Uri workingDir, string fileName )
		{
			XDocument doc = XDocument.Load( stream );

			XElement rootElement = doc.Element( XplfElement_Root );

			// read base paths
			List<ResourceLocation.ResourceLocationInfoBasePath> basePaths = new List<ResourceLocation.ResourceLocationInfoBasePath>();
			foreach ( var basePathElement in rootElement.Elements( XplfElement_Base ) )
			{
				var basePath = new ResourceLocation.ResourceLocationInfoBasePath( new Uri( basePathElement.Value, UriKind.Absolute ), null );

				if ( basePathElement.Attribute( XplfAttribute_Clients ) != null )
				{
					foreach ( var client in basePathElement.Attribute( XplfAttribute_Clients ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( client ) )
						{
							basePath.Clients.Add( client );
						}
					}
				}

				if ( basePathElement.Attribute( XplfAttribute_LocationIds ) != null )
				{
					foreach ( var location in basePathElement.Attribute( XplfAttribute_LocationIds ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( location ) )
						{
							basePath.LocationIds.Add( location );
						}
					}
				}

				basePaths.Add( basePath );
			}

			// read tracks
			XElement playlistElement = rootElement.Element( XplfElement_Playlist );
			Playlist playlist = new Playlist( (playlistElement.Attribute( XplfAttribute_Name ) ?? new XAttribute( XplfAttribute_Name, fileName )).Value );

			foreach ( var trackElement in playlistElement.Elements( XplfElement_Track ) )
			{
				PlaylistItemBase track = ReadTrack( trackElement, workingDir, basePaths );

				if ( track != null )
				{
					playlist.Add( track );
				}
			}

			// read xdata
			foreach ( var dataElement in rootElement.Elements( XplfElement_XData ) )
			{
				if ( dataElement.Attribute( XplfAttribute_Clients ).Value == Controller.AppName )
				{
					XElement currentTrackElement = dataElement.Element( XplfElement_CurrentTrack );

					if ( currentTrackElement != null )
					{
						try
						{
							playlist.SelectedIndex = int.Parse( currentTrackElement.Value );
						}
						catch ( Exception e )
						{
							System.Diagnostics.Debug.Assert( false, "An error occured when loading a playlist:\r\n" + e.ToString() );
						}
					}
				}
			}

			// TODO: maradékot is beolvasni, letárolni, write-ban kiírni

			return playlist;
		}

		// TODO: Update function for xplf files

		/// <summary>
		/// Writes an eXtensible PlayList File on the passed Uri from the passed Playlist
		/// </summary>
		/// <param name="playlist"></param>
		/// <param name="uri"></param>
		/// <returns></returns>
		public static void Write( Playlist playlist, Uri uri )
		{
			using ( var stream = ServiceCenter.FileHandler.GetStream( uri.LocalPath, PortableFileMode.Create, PortableFileAccess.Write, PortableFileShare.None ) )
			{
				Write( playlist, stream );
			}
		}

		/// <summary>
		/// Writes an eXtensible PlayList File on the passed Stream from the passed Playlist
		/// </summary>
		/// <param name="playlist"></param>
		/// <param name="stream"></param>
		/// <returns></returns>
		public static void Write( Playlist playlist, Stream stream )
		{
			XDocument doc = new XDocument();

			XElement rootElement = new XElement( XplfElement_Root );
			rootElement.Add( new XAttribute( XplfAttribute_Version, XplfVersion_Native ) );

			XElement dataElement = new XElement( XplfElement_XData );
			dataElement.Add( new XAttribute( XplfAttribute_Clients, Controller.AppName ) );

			XElement currentTrackElement = new XElement( XplfElement_CurrentTrack );
			XElement playlistElement = new XElement( XplfElement_Playlist );

			if ( !string.IsNullOrWhiteSpace( playlist.Name ) )
			{
				playlistElement.Add( new XAttribute( XplfAttribute_Name, playlist.Name ) );
			}

			try
			{
				currentTrackElement.Value = playlist.SelectedIndex.ToString();
				dataElement.Add( currentTrackElement );

				rootElement.Add( dataElement );

				List<ResourceLocation.ResourceLocationInfoBasePath> basePaths = new List<ResourceLocation.ResourceLocationInfoBasePath>();

				foreach ( var track in playlist )
				{
					XElement trackElement = new XElement( XplfElement_Track );

					WriteTrack( MediaIO.GetResourceLocationOf( track ), trackElement, basePaths );

					playlistElement.Add( trackElement );
				}

				foreach ( var basePath in basePaths )
				{
					XElement baseElement = new XElement( XplfElement_Base );
					baseElement.Value = basePath.AbsolutePath.AbsoluteUri;

					if ( basePath.Clients.Count > 0 )
					{
						baseElement.Add( new XAttribute( XplfAttribute_Clients, string.Join( ",", basePath.Clients ) ) );
					}

					if ( basePath.LocationIds.Count > 0 )
					{
						baseElement.Add( new XAttribute( XplfAttribute_LocationIds, string.Join( ",", basePath.LocationIds ) ) );
					}

					rootElement.Add( baseElement );
				}
			}
			finally
			{
				// TODO: unlock playlist
			}

			rootElement.Add( playlistElement );

			doc.Add( rootElement );
			doc.Save( stream );
		}

		#region Private methods

		/// <summary>
		/// Reads a track from its xml representation in a playlist.
		/// If it cannot find it, returns null.
		/// </summary>
		/// <param name="trackElement"></param>
		/// <param name="workingDir">the path for which the local paths will be considered relative to</param>
		/// <param name="basePaths">the list of possible base paths for relative paths</param>
		/// <returns></returns>
		private static PlaylistItemBase ReadTrack( XElement trackElement, Uri workingDir, IEnumerable<ResourceLocation.ResourceLocationInfoBasePath> basePaths )
		{
			ResourceLocation trackLocation = new ResourceLocation( Controller.WorkingDir, workingDir );

			foreach ( var basePath in basePaths )
			{
				var basePathLocalCopy = new ResourceLocation.ResourceLocationInfoBasePath( basePath.AbsolutePath, trackLocation );
				basePathLocalCopy.Clients.UnionWith( basePath.Clients );
				basePathLocalCopy.LocationIds.UnionWith( basePath.LocationIds );
				trackLocation.BasePaths.Add( basePathLocalCopy );
			}

			ReadKeyOrMetaElement( trackElement.Element( XplfElement_Key ), trackLocation.Key );
			ReadKeyOrMetaElement( trackElement.Element( XplfElement_Meta ), trackLocation.Meta );

			foreach ( var idElement in trackElement.Elements( XplfElement_Id ) )
			{
				var id = new ResourceLocation.ResourceLocationInfoId( idElement.Value, trackLocation );

				if ( idElement.Attribute( XplfAttribute_Clients ) != null )
				{
					foreach ( var client in idElement.Attribute( XplfAttribute_Clients ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( client ) )
						{
							id.Clients.Add( client );
						}
					}
				}

				if ( idElement.Attribute( XplfAttribute_LocationIds ) != null )
				{
					foreach ( var location in idElement.Attribute( XplfAttribute_LocationIds ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( location ) )
						{
							id.LocationIds.Add( location );
						}
					}
				}

				trackLocation.Ids.Add( id );
			}

			foreach ( var relativeElement in trackElement.Elements( XplfElement_Relative ) )
			{
				var relativePath = new ResourceLocation.ResourceLocationInfoRelativePath( relativeElement.Value, trackLocation );

				if ( relativeElement.Attribute( XplfAttribute_Clients ) != null )
				{
					foreach ( var client in relativeElement.Attribute( XplfAttribute_Clients ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( client ) )
						{
							relativePath.Clients.Add( client );
						}
					}
				}

				if ( relativeElement.Attribute( XplfAttribute_LocationIds ) != null )
				{
					foreach ( var location in relativeElement.Attribute( XplfAttribute_LocationIds ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( location ) )
						{
							relativePath.LocationIds.Add( location );
						}
					}
				}

				trackLocation.RelativePaths.Add( relativePath );
			}

			foreach ( var absoluteElement in trackElement.Elements( XplfElement_Absolute ) )
			{
				var absolutePath = new ResourceLocation.ResourceLocationInfoAbsolutePath( new Uri( absoluteElement.Value, UriKind.Absolute ), trackLocation );

				if ( absoluteElement.Attribute( XplfAttribute_Clients ) != null )
				{
					foreach ( var client in absoluteElement.Attribute( XplfAttribute_Clients ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( client ) )
						{
							absolutePath.Clients.Add( client );
						}
					}
				}

				if ( absoluteElement.Attribute( XplfAttribute_LocationIds ) != null )
				{
					foreach ( var location in absoluteElement.Attribute( XplfAttribute_LocationIds ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( location ) )
						{
							absolutePath.LocationIds.Add( location );
						}
					}
				}

				trackLocation.AbsolutePaths.Add( absolutePath );
			}

			foreach ( var localElement in trackElement.Elements( XplfElement_Local ) )
			{
				var localPath = new ResourceLocation.ResourceLocationInfoLocalPath( localElement.Value, trackLocation );

				if ( localElement.Attribute( XplfAttribute_Clients ) != null )
				{
					foreach ( var client in localElement.Attribute( XplfAttribute_Clients ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( client ) )
						{
							localPath.Clients.Add( client );
						}
					}
				}

				if ( localElement.Attribute( XplfAttribute_LocationIds ) != null )
				{
					foreach ( var location in localElement.Attribute( XplfAttribute_LocationIds ).Value.Split( ',' ) )
					{
						if ( !string.IsNullOrWhiteSpace( location ) )
						{
							localPath.LocationIds.Add( location );
						}
					}
				}

				trackLocation.LocalPaths.Add( localPath );
			}

			// TODO: xdaták és (?) ismeretlen elemek beolvasása

			return MediaIO.GetOrCreatePlaylistItemFor( trackLocation );
		}

		/// <summary>
		/// Fills the passed track element with the data from trackLocation, while also adding the missing base paths to the passed list
		/// </summary>
		/// <param name="trackLocation"></param>
		/// <param name="trackElement"></param>
		/// <param name="basePaths"></param>
		private static void WriteTrack( ResourceLocation trackLocation, XElement trackElement, List<ResourceLocation.ResourceLocationInfoBasePath> basePaths )
		{
			foreach ( var basePath in trackLocation.BasePaths )
			{
				ResourceLocation.ResourceLocationInfoBasePath matchingPath =
					(from path
					 in basePaths
					 where path.AbsolutePath.AbsoluteUri == basePath.AbsolutePath.AbsoluteUri
					 select path)
						.FirstOrDefault();

				if ( matchingPath != null )
				{
					matchingPath.Clients.UnionWith( basePath.Clients );
					matchingPath.LocationIds.UnionWith( basePath.LocationIds );
				}
				else
				{
					basePaths.Add( basePath );
				}
			}

			foreach ( var id in trackLocation.Ids )
			{
				XElement idElement = new XElement( XplfElement_Id );

				if ( id.Clients.Count > 0 )
				{
					idElement.Add( new XAttribute( XplfAttribute_Clients, string.Join( ",", id.Clients ) ) );
				}

				if ( id.LocationIds.Count > 0 )
				{
					idElement.Add( new XAttribute( XplfAttribute_LocationIds, string.Join( ",", id.LocationIds ) ) );
				}

				idElement.Value = id.Id;
				trackElement.Add( idElement );
			}

			foreach ( var relativePath in trackLocation.RelativePaths )
			{
				XElement relativeElement = new XElement( XplfElement_Relative );

				if ( relativePath.Clients.Count > 0 )
				{
					relativeElement.Add( new XAttribute( XplfAttribute_Clients, string.Join( ",", relativePath.Clients ) ) );
				}

				if ( relativePath.LocationIds.Count > 0 )
				{
					relativeElement.Add( new XAttribute( XplfAttribute_LocationIds, string.Join( ",", relativePath.LocationIds ) ) );
				}

				relativeElement.Value = relativePath.RelativePath;
				trackElement.Add( relativeElement );
			}

			foreach ( var absolutePath in trackLocation.AbsolutePaths )
			{
				XElement absoluteElement = new XElement( XplfElement_Absolute );

				if ( absolutePath.Clients.Count > 0 )
				{
					absoluteElement.Add( new XAttribute( XplfAttribute_Clients, string.Join( ",", absolutePath.Clients ) ) );
				}

				if ( absolutePath.LocationIds.Count > 0 )
				{
					absoluteElement.Add( new XAttribute( XplfAttribute_LocationIds, string.Join( ",", absolutePath.LocationIds ) ) );
				}

				absoluteElement.Value = absolutePath.AbsolutePath.AbsoluteUri;
				trackElement.Add( absoluteElement );
			}

			foreach ( var localPath in trackLocation.LocalPaths )
			{
				XElement localElement = new XElement( XplfElement_Local );

				if ( localPath.Clients.Count > 0 )
				{
					localElement.Add( new XAttribute( XplfAttribute_Clients, string.Join( ",", localPath.Clients ) ) );
				}

				if ( localPath.LocationIds.Count > 0 )
				{
					localElement.Add( new XAttribute( XplfAttribute_LocationIds, string.Join( ",", localPath.LocationIds ) ) );
				}

				localElement.Value = localPath.RelativePath;
				trackElement.Add( localElement );
			}

			XElement keyElement = new XElement( XplfElement_Key );
			WriteKeyOrMetaElement( keyElement, trackLocation.Key );

			if ( keyElement.HasElements )
			{
				trackElement.Add( keyElement );
			}

			XElement metaElement = new XElement( XplfElement_Meta );
			WriteKeyOrMetaElement( metaElement, trackLocation.Meta );

			if ( metaElement.HasElements )
			{
				trackElement.Add( metaElement );
			}
		}

		/// <summary>
		/// Reads a key or meta element in a track element and sets the parsed data in the passed MediaKey
		/// </summary>
		/// <param name="keyElement"></param>
		/// <param name="mediaKey"></param>
		private static void ReadKeyOrMetaElement( XElement keyElement, MediaKey mediaKey )
		{
			if ( keyElement == null )
			{
				return;
			}

			var hashElement = keyElement.Element( XplfElement_Hash );
			if ( hashElement != null )
			{
				var kindAttribute = hashElement.Attribute( XplfAttribute_HashKind );
				string kind = kindAttribute != null ? kindAttribute.Value : null;

				mediaKey.Hash = new Hash( hashElement.Value, kind );
			}

			// TODO: többi key element beolvasása
		}

		/// <summary>
		/// Writes a key or meta element in a track element from the passed MediaKey
		/// </summary>
		/// <param name="keyElement"></param>
		/// <param name="mediaKey"></param>
		private static void WriteKeyOrMetaElement( XElement keyElement, MediaKey mediaKey )
		{
			if ( mediaKey.Hash != null )
			{
				XElement hashElement = new XElement( XplfElement_Hash );
				hashElement.Add( new XAttribute( XplfAttribute_HashKind, mediaKey.Hash.Algorithm ) );
				hashElement.Value = mediaKey.Hash.HashCode;
				keyElement.Add( hashElement );
			}

			// TODO: többi, xdata is (speciális xdata IO függvények, mindenhol ezt használni)
		}

		#endregion Private methods

		#region Format constants

		private const string XplfVersion_Native = "0.2";

		private const string XplfElement_Root = "xplf";
		private const string XplfElement_Base = "base";
		private const string XplfElement_Playlist = "playlist";
		private const string XplfElement_Track = "track";
		private const string XplfElement_Id = "id";
		private const string XplfElement_Relative = "relative";
		private const string XplfElement_Absolute = "absolute";
		private const string XplfElement_Local = "local";
		private const string XplfElement_Key = "key";
		private const string XplfElement_Meta = "meta";
		private const string XplfElement_Hash = "hash";
		private const string XplfElement_XData = "xdata";
		private const string XplfElement_CurrentTrack = "currentTrack";

		private const string XplfAttribute_Version = "version";
		private const string XplfAttribute_Name = "name";
		private const string XplfAttribute_LocationIds = "locationIds";
		private const string XplfAttribute_Clients = "clients";
		private const string XplfAttribute_HashKind = "kind";

		#endregion Format constants
	}
}
